The XView2 dataset, developed by the Defense Innovation Unit (DIU), is a benchmark dataset designed for assessing building damage using satellite imagery before and after natural disasters. It consists of high-resolution images covering diverse geographic regions, paired with detailed annotations of buildings and their corresponding damage levels. The dataset is structured to facilitate tasks such as semantic segmentation, change detection, and damage classification.
In this project, we focus on building segmentation, where the objective is to identify and delineate structures from satellite imagery accurately. By leveraging the pre-disaster imagery from XView2, my approach aims to construct precise building masks that serve as a foundation for downstream tasks like damage assessment.
The challenges inherent to this dataset, including: occlusions, diverse building architectures, and varying environmental conditions, provide a robust platform for developing and evaluating segmentation models.
The solution proposed utilizes a tailored U-Net architecture with a state-of-the-art backbone to enhance segmentation accuracy, also incorporating domain-specific data selection / augmentations and a combination of loss functions.
To optimize as much as possible the training time use:
Accelerator: TPU VM v3-8
Mixing Precision Policy mixed_bfloat16
Import Libraries
¶import os
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import seaborn as sns
plt.style.use('ggplot')
Exploratory Data Analysis
¶"""
CONFIG DATA
"""
include_post = False
group = 2
BACKBONE = 'SERESNEXT50'
The Dataset consist of satellite images (dimension 1024x1024x3), from different geographical regions. A representative amount of the images has no buildings.
The distribution of images can interfere with segmentation performance. Ignoring Images with no buildings can potentially reduce model training time, however, it can lead to loss of context knowledge. A way to reduce images with no building is sampling images with representative regions (buildings, rives, lakes, etc) from each group of images (source).
from PIL import Image
def read_names(df):
# read directory
base = {1:"/kaggle/input/xview2-assess-building-damage/train_images_labels_targets/train_images_labels_targets/train/",
2:"/kaggle/input/xview2-assess-building-damage/tier3/tier3/"}
names = (df['collection']+"_" + df["id"])
gen_base = base[df['group']]
input_files = gen_base + "images/" + df['collection']+ "_" + df["id"] + "_pre_disaster.png"
output_files = gen_base + "targets/" + df['collection']+ "_" + df["id"] + '_pre_disaster_target.png'
return input_files, output_files
for i, name in enumerate(df['collection'].unique()):
_ = df[df['collection']==name].sample(5*2)
f,axs = plt.subplots(ncols=5, nrows=2, figsize=(10,4))
for (row, ax) in zip(_.iterrows(),axs.ravel()):
f_name, f_name_label = read_names(row[1])
img = Image.open(f_name)
ax.imshow(img)
try:
img_l = Image.open(f_name_label)
ax.imshow(img_l, alpha=0.4, cmap="inferno")
except:
pass
ax.axis("off")
f.suptitle(name, y=0.95)
plt.show()
#if i==0: break
Ignoring Images with no buildings, the building count per image is the following:
# Ignore images with no-buildings
df_nb = df[~(df[['no-damage','minor-damage','major-damage','destroyed']].sum(axis=1)==0)]
f,ax=plt.subplots(figsize=(8, 4), ncols=2)
df_nb[["no-damage","minor-damage","major-damage","destroyed"]].sum(axis=1).hist(bins=np.arange(0, 200, 10), ax=ax[0],
color='indigo', edgecolor='white')
ax[0].set_title("Building Count per Image")
ax[0].text(-0.3, 0.9, 'Bin Width = 10', transform=plt.gca().transAxes, fontsize=10, ha='right')
ax[0].set_xlabel("Building Group")
ax[0].set_ylabel("Count")
ax[1].set_title("Images with \n10 buildings or less")
(df_nb['b_count']<=10).value_counts().plot(kind='bar', color='indigo',ax=ax[1])
#plt.tight_layout()
plt.show()
A representative amount of images has 10 buildings or less, it seems to be a good idea to just ignore images without such low buildings.
However, this does not works so well, as expected, to ignore images with visible no-building structures (mountains, rivers, streets) can lead to misinterpretation at inference.)
from matplotlib.gridspec import GridSpec
plt.rcParams.update(plt.rcParamsDefault) # default settings for matplot
img = Image.open("/kaggle/input/xview2-assess-building-damage/tier3/tier3/images/joplin-tornado_00000001_pre_disaster.png")
crop_img = lambda stride, i, j: np.array(img)[0+stride*i:512+stride*i, 0+stride*j:512+stride*j]
gen_order = lambda a: [(i, j) for i in range(a) for j in range(a)]
# Create the figure
fig = plt.figure(figsize=(12, 4))
# Define the grid layout for the main row (3 sections)
outer_grid = GridSpec(1, 3, figure=fig, wspace=0.3)
# 1. Leftmost plot (1 imshow)
ax_left = fig.add_subplot(outer_grid[0])
ax_left.imshow(img, cmap='viridis')
ax_left.set_title("Original Image")
ax_left.axis("off")
middle_grid = outer_grid[1].subgridspec(2,2)
order = gen_order(2)
for i in range(4):
ax = fig.add_subplot(middle_grid[i])
ax.imshow(crop_img(512, *order[i]), cmap='magma')
ax.axis("off")
fig.text(0.51, 0.9, "Patches with Stride = 512", ha="center", fontsize=12)
right_grid = outer_grid[2].subgridspec(3,3)
order = gen_order(3)
for i in range(9):
ax = fig.add_subplot(right_grid[i])
ax.imshow(crop_img(256, *order[i]), cmap='magma')
ax.axis("off")
fig.text(0.8, 0.9, "Patches with Stride = 256", ha="center", fontsize=12)
plt.show()
High resolution images (1024x1024) can contain a large amount of information, reducing the image into patches (512,512) can improve the quality of the edges obtained from the segmentation model.
An intial U-Net baseline model was created, further analysis shows poorly defined labels.
To reduce noise in model, those poorly labeled samples were identified and excluded. This was possible estimating building pixels difference between labels and predictions.
From the available Data:
To estimate the model performance as realistic as possible, the Test Set is considered with satellite images of unknown geographical region and building distribution.
df_yb = df[~(df[['no-damage','minor-damage','major-damage','destroyed']].sum(axis=1)==0)]
df_nb = df[(df[['no-damage','minor-damage','major-damage','destroyed']].sum(axis=1)==0)]
limit_group = 500
df_n = pd.DataFrame()
for name in df_nb['collection'].unique(): #groupby('collection')['id'].count()
temp_df = df[df['collection']==name]
# downsample no-building images
if len(temp_df)>limit_group:
temp_df = temp_df.sample(n=limit_group, replace=False, random_state=2024)
df_n = pd.concat([df_n, temp_df], axis=0, ignore_index=True)
from sklearn.model_selection import train_test_split
train, test = train_test_split(df_n, random_state=2024, test_size=0.1) # 0.25
meta_tr = train.groupby("collection")['id'].count().to_dict()
meta_ts = test.groupby("collection")['id'].count().to_dict()
######### Ignore poorly defined label samples
ignore_l = ["socal-fire_00001233","hurricane-harvey_00000107",
"hurricane-harvey_00000138","socal-fire_00001313",
"hurricane-harvey_00000245","moore-tornado_00000142",
"lower-puna-volcano_00000089"]
train = train[~(train['collection']+"_"+train['id']).isin(ignore_l)]
#########
#for k in meta_tr.keys():
# print(f"{k:<20} {meta_tr[k]:>5} {meta_ts.get(k):}")# {meta_tr[k]/meta_ts[k]:>3}")
def read_names(df, from_json=False, include_post=False):
# read directory
base1 = "/kaggle/input/xview2-assess-building-damage/train_images_labels_targets/train_images_labels_targets/train/"
base2 = "/kaggle/input/xview2-assess-building-damage/tier3/tier3/"
names = (df['collection']+"_" + df["id"])
gen_base = df['group'].apply(lambda t: base1 if t==1 else base2)
input_files = gen_base + "images/" + df['collection']+ "_" + df["id"] + "_pre_disaster.png"
if from_json:
output_files = gen_base + "labels/" + df['collection']+ "_" + df["id"] + '_pre_disaster.json'
else:
output_files = gen_base + "targets/" + df['collection']+ "_" + df["id"] + '_pre_disaster_target.png'
if include_post:
"""
include '_post_disaster' images without damage
"""
temp = df[df[['minor-damage','major-damage','destroyed']].astype('int').sum(axis=1)==0].copy()
gen_base2 = temp['group'].apply(lambda t: base1 if t==1 else base2)
input_files2 = gen_base2 + "images/" + temp['collection']+ "_" + temp["id"] + "_post_disaster.png"
if from_json:
output_files2 = gen_base2 + "labels/" + temp['collection']+ "_" + temp["id"] + '_post_disaster.json'
else:
output_files2 = gen_base2 + "targets/" + temp['collection']+ "_" + temp["id"] + '_post_disaster_target.png'
input_files = pd.concat([input_files,input_files2], ignore_index=True)
output_files = pd.concat([output_files,output_files2], ignore_index=True)
return input_files.values, output_files.values
train_names = read_names(train, from_json=True, include_post=include_post)
test_names = read_names(test, from_json=True, include_post=include_post)
print("Train Files: ", len(train_names[0]))
print("Validation Files: ", len(test_names[0]))
Train Files: 5064 Validation Files: 564
import tensorflow as tf
from tensorflow.keras import layers
tpu = None
try:
tpu = tf.distribute.cluster_resolver.TPUClusterResolver()
tf.config.experimental_connect_to_cluster(tpu)
tf.tpu.experimental.initialize_tpu_system(tpu)
#`tf.distribute.experimental.TPUStrategy` is deprecated
strategy = tf.distribute.TPUStrategy(tpu)
except:
strategy = tf.distribute.get_strategy()
from tensorflow.keras import mixed_precision
# NVIDIA GPUs support using a mix of float16 and float32,
# while TPUs and Intel CPUs support a mix of bfloat16 and float32.
if 'tpu' in strategy.cluster_resolver.__str__():
print('Using TPU mixed precision')
policy = mixed_precision.Policy('mixed_bfloat16')
else:
policy = mixed_precision.Policy('mixed_float16')
mixed_precision.set_global_policy(policy)
Using TPU mixed precision
!pip install shapely
!!pip install -U segmentation-models # automatic U-Net implementation for tf.keras
#!pip install -U image-classifiers # collection of novel classification models (2019), already installed with segmentation-models
from PIL import Image, ImageDraw
from shapely import wkt
import numpy as np
import json
# create binary mask from label json file
def create_build_from_json(file_path):
data = tf.io.read_file(file_path)
data = json.loads(data.numpy().decode('utf-8'))
img = Image.new('L', (1024, 1024), color=0) #'black')
draw = ImageDraw.Draw(img, 'L')
#color = (255) #, 255, 255, 125) # white
for cnt, polygon in enumerate( data['features']['xy'] ):
x,y = wkt.loads(polygon['wkt']).exterior.coords.xy
coords = list(zip(x,y))
draw.polygon(coords, fill=1)
return np.array(img)[...,np.newaxis] # dtype uint8
import tensorflow as tf
from tensorflow.keras import backend
from scipy.ndimage import distance_transform_edt as distance
print(tf.__version__)
def read_image_tf(path):
# Read the contents of the image file as a string
temp = tf.io.read_file(path)
# Decode the string into a tensor
return tf.io.decode_image(temp)
# preprocess function to manage file reading
def preprocess_load_file(use_json=False):
def funct_inter(img_path, target_path):
image = read_image_tf(img_path)
if use_json:
label = tf.py_function(func=create_build_from_json, inp=[target_path], Tout=tf.uint8)
else:
label = read_image_tf(target_path)
# Normalize Image
image = tf.cast(image, tf.float32)
label = tf.cast(label, tf.float32)
# Scale the pixel values to the range [0, 1]
image = image / 255.0
# set shape information explicitly
image.set_shape([1024,1024,3])
label.set_shape([1024,1024,1])
return image, label
return funct_inter
2.16.1
def crop_image(input_size, crop_size, stride):
"""
Crop images from 1024x1024 to 512x512 patches from image
inputs shape: (rows. columns. channels) -> output (#splits, rows. columns. channels)
"""
def inner_funct(image, label):
patches = []
for i in range(0, input_size, stride):
for j in range(0, input_size, stride):
# patches inside image only
if (i+crop_size)>input_size or (j+crop_size)>input_size:
continue
patch_im = tf.image.crop_to_bounding_box(image, i, j, crop_size, crop_size)
patch_lb = tf.image.crop_to_bounding_box(label, i, j, crop_size, crop_size)
patches.append( (patch_im, patch_lb) )
return tf.data.experimental.from_list(patches)
return inner_funct
BATCH_SIZE = 9 #8 #GPU #1*9 #TPU
GLOBAL_BATCH_SIZE = BATCH_SIZE*strategy.num_replicas_in_sync
print(GLOBAL_BATCH_SIZE) # 256 in TPU
72
from tensorflow.keras.layers import Layer
class BinarizeMaskLayer(Layer):
def __init__(self, threshold=0.5, **kwargs):
super(BinarizeMaskLayer, self).__init__(**kwargs)
self.threshold = threshold
def call(self, inputs):
# Apply the binarization threshold across the batch
return tf.where(inputs > self.threshold, 1.0, 0.0)
def get_config(self):
config = super().get_config()
config.update({"threshold": self.threshold})
return config
# simple random flip applied to whole batch
class Augment(Layer):
def __init__(self, seed=42):
super().__init__()
# both use the same seed, so they'll make the same random changes.
# **MUST TWO** times cause, each call can generate differente effects
self.aug_inputs = tf.keras.Sequential([
tf.keras.layers.RandomRotation(factor=0.2, fill_mode="constant", seed=seed),
tf.keras.layers.RandomFlip(mode='horizontal_and_vertical', seed=seed),
tf.keras.layers.RandomTranslation(height_factor=0.1, width_factor=0.1, fill_mode="constant", seed=seed),
tf.keras.layers.RandomZoom(height_factor=0.2, width_factor=0.2, fill_mode='constant', seed=seed),
tf.keras.layers.RandomBrightness(factor=0.1, value_range=(0.0, 1.0), seed=seed),
tf.keras.layers.RandomContrast(factor=0.1, seed=seed)
])
self.aug_labels = tf.keras.Sequential([
tf.keras.layers.RandomRotation(factor=0.2, fill_mode="constant", seed=seed),
tf.keras.layers.RandomFlip(mode='horizontal_and_vertical', seed=seed),
tf.keras.layers.RandomTranslation(height_factor=0.1, width_factor=0.1, fill_mode="constant", seed=seed),
tf.keras.layers.RandomZoom(height_factor=0.2, width_factor=0.2, fill_mode='constant', seed=seed),
BinarizeMaskLayer(threshold=0.5) # to avoid loss precision in border resolution
])
def call(self, inputs, labels):
inputs = self.aug_inputs(inputs)
labels = self.aug_labels(labels)
return inputs, labels
def has_nonzero_label(image, label):
# Check if there are any non-zero elements in the label
limit = int(512*512 * 0.025) # at leat 2.5% of building pixels per image
mask_sum = tf.reduce_sum(tf.cast(tf.not_equal(label, 0), dtype="int32"))
return mask_sum >= limit
#return tf.reduce_any(tf.not_equal(label, 0))
"""
CONFIG
"""
input_size=1024
crop_size = 512
crop_function_train = crop_image(input_size, crop_size, stride=512//2)
crop_function_test = crop_image(input_size, crop_size, stride=512)
"""
Dataset Creation
"""
ds_train = tf.data.Dataset.from_tensor_slices((train_names[0],train_names[1]))
ds_train = (ds_train.map(preprocess_load_file(use_json=True),
num_parallel_calls=tf.data.AUTOTUNE)
# Map the crop function to each image, flat_map is used to flatten the list of patches
.flat_map(lambda image, label: (crop_function_train(image, label)))
#.filter(has_nonzero_label)
.cache()
.shuffle(buffer_size = 8000*2) #300) #8000) #len(train_names[0])*9) #15000) #len(train_names[0])*9 = 18000
.batch(GLOBAL_BATCH_SIZE)
#.map(Augment(2024), num_parallel_calls=tf.data.AUTOTUNE)
.prefetch(tf.data.AUTOTUNE)
)
ds_test = tf.data.Dataset.from_tensor_slices((test_names[0],test_names[1]))
ds_test = (ds_test.map(preprocess_load_file(use_json=True),
num_parallel_calls=tf.data.AUTOTUNE)
.flat_map(lambda image, label: (crop_function_test(image, label)))
#.filter(has_nonzero_label)
.cache()
.batch(GLOBAL_BATCH_SIZE)
.prefetch(tf.data.AUTOTUNE))
t_image, t_label = ds_test.take(1).as_numpy_iterator().next()
t_image.shape, t_label.shape
((72, 512, 512, 3), (72, 512, 512, 1))
Model Training
¶U-Net is an effective way to evaluate image segmentation, the trick part is to adapt is architecture to be effective, in this case:
BACKBONE: SE-ResNeXt50, pretrained on Imagenet
Asymmetric Convolution Block (ACB) at Decoder
Loss Function
Focal Loss helps to focus training on unbalanced classes, Dice loss is prefered for segmentation tasks.
import os
os.environ["SM_FRAMEWORK"] = "tf.keras"
import segmentation_models as sm # ignore in TPU env
from classification_models.tfkeras import Classifiers
Segmentation Models: using `tf.keras` framework.
from tensorflow.keras.layers import Conv2D, Reshape, Multiply, Permute, Activation, Dot, Add, MaxPooling2D, UpSampling2D
from tensorflow.keras import layers
class AttentionGate(tf.keras.layers.Layer):
def __init__(self, filters, name):
super(AttentionGate, self).__init__(name=f"AttentionGate_{name}")
self.sigmoid = Activation('sigmoid')
self.relu = layers.ReLU()
# Define the convolutions
self.conv_o = Conv2D(filters, kernel_size=1) # representation of the current pixel.
self.conv_s = Conv2D(filters, kernel_size=1) # Representation of the pixels you’re attending to.
self.psi = Conv2D(1, kernel_size=1)
def call(self, inputs, skip):
out_a = self.conv_o(inputs) # [batch_size, height, width, filters // 2]
skip_a = self.conv_s(skip) # [batch_size, height, width, filters // 2]
psi_a = self.psi(self.relu(out_a + skip_a)) # [batch_size, height, width, 1]
attention = self.sigmoid(psi_a) # [batch_size, height, width, 1]
skip = skip * attention
return skip
from tensorflow.keras.layers import (Conv2D, Conv2DTranspose, Concatenate,
UpSampling2D, Add, Activation, BatchNormalization)
def asymmetric_conv_block(input_tensor, filters, n_sx=''):
# 1x3 convolution
conv1 = Conv2D(filters, (1, 3), padding='same', name=n_sx+'_cv1')(input_tensor)
# 3x1 convolution
conv2 = Conv2D(filters, (3, 1), padding='same', name=n_sx+'_cv2')(input_tensor)
# 3x3 convolution
conv3 = Conv2D(filters, (3, 3), padding='same', name=n_sx+'_cv3')(input_tensor)
# Combine the outputs of the asymmetric convolutions
x = Add()([conv1, conv2, conv3])
x = BatchNormalization(name=n_sx+'_bn')(x)
x = Activation('relu')(x)
return x
def decode_block(filters, x, concat_l, n_sx='', include_post_conv=True, include_attention_gate=False):
x = Conv2DTranspose(filters, (3, 3), strides=(2, 2), padding='same', activation='swish', name=n_sx+'_conv_trans')(x)
if len(concat_l)!=0:
if include_attention_gate:
skip_out = AttentionGate(filters=filters, name=n_sx)(x, concat_l[0])
x = Concatenate()([x, skip_out])
else:
x = Concatenate()([x]+concat_l) #[el for el in concat_l])
if include_post_conv:
x = asymmetric_conv_block(x, filters=filters, n_sx=n_sx+'_acb')
return x
def generate_model(enc_model, enc_layers):
if enc_layers[0]==-1:
enc5, enc4, enc3, enc2, enc1 = [enc_model.layers[name].output for name in enc_layers ]
else:
enc5, enc4, enc3, enc2, enc1 = [enc_model.get_layer(name=name).output for name in enc_layers]
dec1 = decode_block(filters=512, x=enc5, concat_l=[enc4], n_sx='dec1', include_attention_gate=False)
#print(dec1.shape)
dec2 = decode_block(filters=256, x=dec1, concat_l=[enc3], n_sx='dec2', include_attention_gate=False)
#print(dec2.shape)
dec3 = decode_block(filters=128, x=dec2, concat_l=[enc2], n_sx='dec3', include_attention_gate=False)
#print(dec3.shape)
## extra conections
## kernel should be twice the size of strides to be similar to "bilinear"
##extra1 = Conv2DTranspose(filters=64, kernel_size=(16, 16), strides=(8, 8), padding='same')(dec1)
##extra2 = Conv2DTranspose(filters=64, kernel_size=(8, 8), strides=(4, 4), padding='same')(dec2)
#extra1 = UpSampling2D(size=(8,8), interpolation="bilinear")(dec1)
#extra1 = Conv2D(64, kernel_size=(3,3), padding='same',activation="relu", name='ext1_cv')(extra1)
#extra2 = UpSampling2D(size=(4,4), interpolation="bilinear")(dec2)
#extra2 = Conv2D(64, kernel_size=(3,3), padding='same',activation="relu", name='ext2_cv')(extra2)
dec4 = decode_block(filters=64, x=dec3, concat_l=[enc1], #extra1, extra2],
n_sx='dec4', include_attention_gate=False)
# Output layer
x = Conv2DTranspose(16, (3, 3), strides=(2, 2), padding='same')(dec4)
#print(x.shape)
outputs = Conv2D(1, (1, 1), activation='sigmoid', dtype="float32")(x)
#print(outputs.shape)
# Create model
return tf.keras.Model(inputs=enc_model.input, outputs=outputs)
Metrics Selected:
# Obtain alpha
"""
total = 0
positive_c = 0
for i, (t1_image, (t1_label, _)) in tqdm(enumerate(ds_train)):
# to use float16 labels
positive_c += int(tf.reduce_sum(tf.cast(t1_label, dtype="float32")).numpy())
total += tf.size(t1_label).numpy()
alpha = np.round( (total - positive_c)/total, 2)
print("alpha ", alpha)
"""
#alpha = 0.88 # train +tier3 # no empty slices
alpha = 0.9
from segmentation_models.base import Metric
import segmentation_models.base.functional as F
SMOOTH = 1e-5
class IOUScore(Metric):
r""" The `Jaccard index`_, also known as Intersection over Union and the Jaccard similarity coefficient
(originally coined coefficient de communauté by Paul Jaccard), is a statistic used for comparing the
similarity and diversity of sample sets. The Jaccard coefficient measures similarity between finite sample sets,
and is defined as the size of the intersection divided by the size of the union of the sample sets:
.. math:: J(A, B) = \frac{A \cap B}{A \cup B}
"""
def __init__(
self,class_weights=None,class_indexes=None,threshold=0.5,
per_image=False,smooth=SMOOTH,name=None,):
name = name or 'iou_score'
super().__init__(name=name)
self.class_weights = class_weights if class_weights is not None else 1
self.class_indexes = class_indexes
self.threshold = threshold
self.per_image = per_image
self.smooth = smooth
def __call__(self, gt, pr):
# Explicitly cast inputs to float32 for consistency
gt = tf.cast(gt, tf.float32)
pr = tf.cast(pr, tf.float32)
return F.iou_score(gt,pr,
class_weights=self.class_weights,class_indexes=self.class_indexes,
smooth=self.smooth,
per_image=self.per_image, threshold=self.threshold, **self.submodules
)
class FScore(Metric):
r"""The F-score (Dice coefficient) can be interpreted as a weighted average of the precision and recall,
where an F-score reaches its best value at 1 and worst score at 0.
The relative contribution of ``precision`` and ``recall`` to the F1-score are equal.
The formula for the F score is:
.. math:: F_\beta(precision, recall) = (1 + \beta^2) \frac{precision \cdot recall}
{\beta^2 \cdot precision + recall}
"""
def __init__(self, beta=1, class_weights=None, class_indexes=None,
threshold=None, per_image=False, smooth=SMOOTH,name=None,):
name = name or 'f{}-score'.format(beta)
super().__init__(name=name)
self.beta = beta
self.class_weights = class_weights if class_weights is not None else 1
self.class_indexes = class_indexes
self.threshold = threshold
self.per_image = per_image
self.smooth = smooth
def __call__(self, gt, pr):
# Explicitly cast inputs to float32 for consistency
gt = tf.cast(gt, tf.float32)
pr = tf.cast(pr, tf.float32)
return F.f_score(gt,pr,
beta=self.beta,
class_weights=self.class_weights,
class_indexes=self.class_indexes,
smooth=self.smooth,
per_image=self.per_image,
threshold=self.threshold,
**self.submodules
)
class CombLoss:
def __init__(self, from_logits=False):
self.from_logits = from_logits
self.dice_loss = tf.keras.losses.Dice()
self.binary_focal_loss = tf.keras.losses.BinaryFocalCrossentropy()
def __call__(self, y_true, y_pred):
loss = self.dice_loss(y_true, y_pred) + 10.0 * self.binary_focal_loss(y_true, y_pred)
return loss
with strategy.scope():
enc_layers = (-1, 1078, 584, 254, 4)
classifier, preprocess_input = Classifiers.get('seresnext50') # 'seresnext50': (1078, 584, 254, 4)
encoder_model = classifier(input_shape=(512,512,3), weights=None,# 'imagenet',
include_top=False)
#encoder_model.trainable=True
model = generate_model(enc_model=encoder_model, enc_layers=enc_layers)
model.load_weights("/kaggle/input/building-segmentation-model-for-satellite-imagery/bseg_unet_SERESNEXT50_v38_tpu.weights.h5")
model.compile(
tf.keras.optimizers.AdamW(learning_rate=0.000025),#15), #'Adam', #keras.optimizers.SGD(learning_rate=1e-5, momentum=0.9),
loss=CombLoss,# surface_loss_keras(factor), #binary_focal_dice_loss, #sm.losses.bce_jaccard_loss,
metrics=[IOUScore(threshold=0.5), #tf.keras.metrics.BinaryIoU(target_class_ids=[0, 1], threshold=0.5),
FScore(beta=0.5)],
)
I0000 00:00:1732859035.673834 13 device_compiler.h:188] Compiled cluster using XLA! This line is logged at most once for the lifetime of the process.
With this, the size of the model is:
from IPython.display import clear_output
selected_slice = 27
def display(display_list):
plt.figure(figsize=(15, 15))
title = ['Input Image', 'True Mask', 'Predicted Mask']
for i in range(len(display_list)):
plt.subplot(1, len(display_list), i+1)
plt.title(title[i])
if i>=1:
plt.imshow(display_list[i], vmin=0, vmax=1, cmap="gray")
else:
plt.imshow(display_list[i])
plt.axis('off')
plt.show()
def show_predictions():
display([t_image[selected_slice],
t_label[selected_slice],
model.predict(t_image)[selected_slice].astype('float')])
class DisplayCallback(tf.keras.callbacks.Callback):
def on_epoch_end(self, epoch, logs=None):
clear_output(wait=True)
show_predictions()
print('\nSample Prediction after epoch {}\n'.format(epoch+1))
#from transformers.modeling_tf_utils import keras
early_stopping = tf.keras.callbacks.EarlyStopping(monitor='val_loss', #'val_iou_score',
patience=4, mode='min', verbose=1,
min_delta=0, restore_best_weights=True)
# To reduce Python overhead and maximize the performance of your TPU, Produce erros with generator dataset
# pass in the steps_per_execution argument to Keras Model.compile.
#steps_per_epoch = len(train_names[0]) // (GLOBAL_BATCH_SIZE*4)
#validation_steps = len(test_names[0]) // (GLOBAL_BATCH_SIZE*4)
history = model.fit(x=ds_train, epochs=128,
#steps_per_epoch=steps_per_epoch,
validation_data=ds_test, #validation_steps=validation_steps,
callbacks=[DisplayCallback(),
early_stopping])
from IPython.display import FileLink
# Save the entire model to a HDF5 file.
with strategy.scope():
model.save_weights(f'bseg_unet_{BACKBONE}_v38_tpu.weights.h5')
FileLink(f'bseg_unet_{BACKBONE}_v38_tpu.weights.h5')
show_predictions()
3/3 ━━━━━━━━━━━━━━━━━━━━ 1s 238ms/step
These are other examples:
t_pred = model.predict(t_image) #.logits
#t_pred = tf.math.argmax(t_pred, axis=1) # create mask
for i in range(5,GLOBAL_BATCH_SIZE,GLOBAL_BATCH_SIZE//4):
f, ax =plt.subplots(ncols=3, figsize=(10,5))
ax[0].imshow(t_image[i])
ax[1].imshow(t_label[i], alpha=1.0, vmin=0, vmax=1, cmap="gray")
ax[2].imshow(t_pred[i]>0.5, alpha=1.0, vmin=0, vmax=1, cmap="gray")
for _ in range(3):
ax[_].axis("off")
plt.show()
3/3 ━━━━━━━━━━━━━━━━━━━━ 1s 233ms/step
r1 = tf.image.rot90(t_image, k=1)
r2 = tf.image.rot90(t_image, k=2)
r3 = tf.image.rot90(t_image, k=3)
pred_l = [model.predict(el) for el in [t_image, r1,r2,r3]]
r1_inv = tf.image.rot90(pred_l[1], k=-1)
r2_inv = tf.image.rot90(pred_l[2], k=-2)
r3_inv = tf.image.rot90(pred_l[3], k=-3)
3/3 ━━━━━━━━━━━━━━━━━━━━ 1s 247ms/step 3/3 ━━━━━━━━━━━━━━━━━━━━ 1s 253ms/step 3/3 ━━━━━━━━━━━━━━━━━━━━ 1s 250ms/step 3/3 ━━━━━━━━━━━━━━━━━━━━ 1s 259ms/step
f,ax = plt.subplots(figsize=(10,4), ncols=5)
ax[0].imshow(t_label[selected_slice], cmap="gray"); ax[0].set_title("Original")
ax[1].imshow(pred_l[0][selected_slice], cmap="gray"); ax[1].set_title("Pred")
ax[2].imshow(r1_inv[selected_slice], cmap="gray"); ax[2].set_title("Pred rot90")
ax[3].imshow(r2_inv[selected_slice], cmap="gray"); ax[3].set_title("Pred rot180")
ax[4].imshow(r3_inv[selected_slice], cmap="gray"); ax[4].set_title("Pred rot270")
for _ in ax.ravel():
_.axis("off")
plt.show()
Despite the fact that convolutions allow to reduce the features position/angle prediction dependance, predictions from rotated inputs tends to differ. But if we create a rotated predictions, the mean average of them can be more robust without retraining model.
mean_pred = (pred_l[0] + r1_inv + r2_inv + r3_inv)/4
f,ax=plt.subplots(ncols=3)
i= selected_slice
ax[0].imshow(t_label[i], cmap="gray")
ax[1].imshow(pred_l[0][i], cmap="gray")
ax[2].imshow(mean_pred[i], cmap="gray")
ax[0].set_title("Label")
ax[1].set_title("Prediction")
ax[2].set_title("Processed prediction")
for _ in ax.ravel():
_.axis("off")
plt.show()
from tensorflow.keras.layers import Average
class Rot90Layer(tf.keras.layers.Layer):
"""
A custom layer to rotate images by 90 degrees.
Args:
rotation (int): Number of 90-degree rotations. Positive values rotate counterclockwise.
"""
def call(self, inputs, rotations=1):
return tf.image.rot90(inputs, k=rotations)
def post_model(model):
image = tf.keras.Input(shape=(512,512,3))
rot_images = [Rot90Layer()(image, rotations=i) for i in range(1, 4)]
pred_l = [model(image)] + [model(rot_image) for rot_image in rot_images]
pred_l = [Rot90Layer()(pred, rotations=-1*i) for i, pred in enumerate(pred_l)]
output = Average(dtype="float32")(pred_l)
return tf.keras.Model(inputs=image, outputs=output)
with strategy.scope():
### base model
enc_layers = (-1, 1078, 584, 254, 4)
classifier, preprocess_input = Classifiers.get('seresnext50') # 'seresnext50': (1078, 584, 254, 4)
encoder_model = classifier(input_shape=(512,512,3), weights=None,include_top=False)
encoder_model.trainable=True
model_base = generate_model(enc_model=encoder_model, enc_layers=enc_layers)
model_base.load_weights("/kaggle/input/building-segmentation-model-for-satellite-imagery/bseg_unet_SERESNEXT50_v38_tpu.weights.h5")
#model_base = model
########## post model
post_m = post_model(model_base)
post_m.trainable=False
post_m.compile(
tf.keras.optimizers.AdamW(learning_rate=0.00015),
loss= lambda y_true, y_pred: 0.0, # Dummy Loss
metrics=[IOUScore(threshold=0.4), # Low considering that noise was reduced as much as possible
FScore(beta=0.5)],
)
These are some examples:
t_pred = post_m.predict(t_image) #.logits
#t_pred = tf.math.argmax(t_pred, axis=1) # create mask
for i in range(5,GLOBAL_BATCH_SIZE,GLOBAL_BATCH_SIZE//4):
f, ax =plt.subplots(ncols=3, figsize=(10,5))
ax[0].imshow(t_image[i])
ax[1].imshow(t_label[i], alpha=1.0, vmin=0, vmax=1, cmap="gray")
ax[2].imshow(t_pred[i]>0.5, alpha=1.0, vmin=0, vmax=1, cmap="gray")
ax[0].set_title(i)
for _ in range(3):
ax[_].axis("off")
plt.show()
3/3 ━━━━━━━━━━━━━━━━━━━━ 2s 596ms/step
results = model.evaluate(ds_test)
print("Val Loss: ",results[0], "IOU Score: ", results[1], "f0.5-score: ", results[2])
32/32 ━━━━━━━━━━━━━━━━━━━━ 15s 436ms/step - f0.5-score: 0.7331 - iou_score: 0.7160 - loss: 0.3306 Val Loss: 0.3356271982192993 IOU Score: 0.737855076789856 f0.5-score: 0.7313465476036072
results = post_m.evaluate(ds_test)
print("Val Loss: ",results[0], "IOU Score: ", results[1], "f0.5-score: ", results[2])
32/32 ━━━━━━━━━━━━━━━━━━━━ 45s 1s/step - f0.5-score: 0.6805 - iou_score: 0.6856 - loss: 0.0000e+00 Val Loss: 0.0 IOU Score: 0.697926938533783 f0.5-score: 0.6762031316757202
Validation Set Metrics
| Model | IOU Score: 0.737 | f0.5-score: 0.731 |
| Model with Post-processing | IOU Score: 0.697 | f0.5-score: 0.676 |
Quality vs Quantity
Potential Trade-Off: Post-processing might be sacrificing some of the segmentation precision (in terms of strict overlap with ground truth) for visual improvements (less noise and smoother results).
If reducing artifacts is crucial (e.g., for tasks requiring smooth boundaries), then improving visual quality via post-processing might be a worthwhile trade-off, even if it results in a slight drop in the metric.
Test set performance
¶base2 = "/kaggle/input/xview2-assess-building-damage/test_images_labels_targets/test_images_labels_targets/test/"
images_test = tf.io.gfile.listdir(base2+"images")
images_test = [el.split('_pre')[0] for el in images_test if len(el.split('_pre'))==2]
############ ignore poorly defined labels
m_l = ['socal-fire_00001378', 'hurricane-harvey_00000133', 'socal-fire_00001029']
images_test = [el for el in images_test if el not in m_l]
############
images = [base2+'images/' + el + '_pre_disaster.png' for el in images_test]
targets = [base2+'targets/'+ el + '_pre_disaster_target.png'for el in images_test]
ds_test2 = tf.data.Dataset.from_tensor_slices((images, targets))
ds_test2 = (ds_test2.map(preprocess_load_file(use_json=False),
num_parallel_calls=tf.data.AUTOTUNE)
.flat_map(lambda image, label: (crop_function_test(image, label)))
.batch(GLOBAL_BATCH_SIZE)
.prefetch(tf.data.AUTOTUNE))
results = model.evaluate(ds_test2)
print("Val Loss: ",results[0], "IOU Score: ", results[1], "f0.5-score: ", results[2])
52/52 ━━━━━━━━━━━━━━━━━━━━ 27s 499ms/step - f0.5-score: 0.7924 - iou_score: 0.7594 - loss: 0.3568 Val Loss: 0.370125949382782 IOU Score: 0.7451341152191162 f0.5-score: 0.7824866771697998
results = post_m.evaluate(ds_test2)
print("Val Loss: ",results[0], "IOU Score: ", results[1], "f0.5-score: ", results[2])
52/52 ━━━━━━━━━━━━━━━━━━━━ 75s 1s/step - f0.5-score: 0.7382 - iou_score: 0.7169 - loss: 0.0000e+00 Val Loss: 0.0 IOU Score: 0.7113664150238037 f0.5-score: 0.7221629023551941
Test Set Metrics
| Model | IOU Score: 0.745 | f0.5-score: 0.782 |
| Model with Post-processing | IOU Score: 0.711 | f0.5-score: 0.722 |
ds_test2 = tf.data.Dataset.from_tensor_slices((images, targets))
ds_test2 = (ds_test2.map(preprocess_load_file(flag=False, use_json=False), #cont=[mean, std]),
num_parallel_calls=tf.data.AUTOTUNE)
#.flat_map(lambda image, label: (crop_function_test(image, label)))
.batch(GLOBAL_BATCH_SIZE)
.prefetch(tf.data.AUTOTUNE))
t2_image, t2_label = ds_test2.take(1).as_numpy_iterator().next()
t2_pred = model.predict(t2_image)
for i in [6,8,10,11, 14]:
f,ax=plt.subplots(ncols=3, figsize=(10,4))
ax[0].imshow(t2_image[i])
ax[1].imshow(t2_label[i], cmap="gray")
ax[2].imshow(t2_pred[i], cmap="gray")
ax[0].set_title("Image")
ax[1].set_title("Label")
ax[2].set_title("Prediction")
for _ in range(3):
ax[_].axis("off")
plt.show() #
During the development of this project a selection of images was established. The XView2 dataset contains a very small number of misalignments, artifacts, or poorly defined labels, which have already been observed in other work. This can be further improved with more diverse satellite images from other sources.
The implemented U-Net model can be refined through further research. Specifically, backbones can play a critical role, however, it was noted that Segformer does not appear to outperform seresnext50.
Dice Loss along with Focal Loss played a fundamental role during model training.